This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and no longer maintained.

Development continues in the completely reworked repository: ZekStack/Tempo

ESPDate

ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript. It wraps time_t / struct tm and adds safe arithmetic, comparisons, and formatting in a single class-based API.

CI / Release / License

CIReleaseLicense: MIT

Features

  • DateTime wrapper: one small DateTime value type instead of juggling raw time_t + struct tm.
  • Safe arithmetic helpers: add/subSeconds, add/subMinutes, add/subHours, add/subDays, add/subMonths, add/subYears.
  • Differences & comparisons: differenceIn*, isBefore, isAfter, isEqual, isSameDay.
  • Minute-level comparisons: isEqualMinutes/isEqualMinutesUtc for coarse equality.
  • Calendar helpers: startOfDay*, endOfDay*, startOfMonth*, endOfMonth*, isLeapYear, daysInMonth, getters for year/month/day/weekday.
  • Formatting / parsing: ISO-8601 and YYYY-MM-DD HH:MM:SS helpers, plus strftime-style patterns for UTC or local time.
  • String helpers: embedded-safe buffer methods and std::string convenience wrappers for DateTime, LocalDateTime, nowUtc, and nowLocal.
  • Direct value formatting: DateTime::localString/utcString and LocalDateTime::localString let individual values format themselves.
  • Sunrise / sunset: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
  • DST detection: isDstActive reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
  • Moon phase: moonPhase returns the current lunar phase angle and illumination fraction for any moment.
  • Optional NTP bootstrap: call init with ESPDateConfig containing timeZone and at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to set TZ and start SNTP after Arduino/WiFi is ready.
  • NTP sync callback + listeners + manual re-sync: register setNtpSyncCallback(...) plus additive addNtpSyncListener(...) observers, call syncNTP() anytime to trigger an immediate refresh, and optionally override SNTP interval via ntpSyncIntervalMs / setNtpSyncIntervalMs(...).
  • Optional PSRAM-backed config/state buffers: ESPDateConfig::usePSRAMBuffers routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through ESPBufferManager with automatic fallback.
  • Explicit lifecycle cleanup: deinit() unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
  • Init-state introspection: isInitialized() reports whether init(...) has been called without a matching deinit().
  • Last sync tracking: hasLastNtpSync() / lastNtpSync() expose the latest SNTP sync timestamp kept inside ESPDate.
  • Last sync string helpers: lastNtpSyncStringLocal/Utc provide direct formatting helpers for lastNtpSync.
  • Local breakdown helpers: nowLocal() / toLocal() surface the broken-out local time (with UTC offset) for quick DST/debug checks; feed sunrise/sunset results into toLocal to read them in local time.
  • Friendly month names: monthName(int|DateTime) returns "January""December" for quick labels.
  • Class-based API: everything hangs off a single ESPDate instance; no global namespace clutter.
  • Lightweight & portable: C++17, header-first public API; relies only on standard C time functions and the system clock (time()).

ESPDate does not configure SNTP by default. Call init with a POSIX TZ string plus at least one NTP server (ntpServer, optional ntpServer2/ntpServer3) to have ESPDate call configTzTime for you. Do this after the Arduino runtime and WiFi are up to avoid early watchdog resets. Otherwise you remain in control of time-zone setup and system clock sync. syncNTP() returns true only when one or more NTP servers are configured and the runtime supports configTzTime. SNTP exposes a system-level sync hook, so the last setNtpSyncCallback(...) registration is the active callback. For the same reason, lastNtpSync() is tracked on the currently active ESPDate instance. addNtpSyncListener(...) attaches extra observers on that active instance without replacing the primary callback; use the returned token with removeNtpSyncListener(...) to detach them. Example member-method binding style: date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1)); Set interval from config or at runtime: date.setNtpSyncIntervalMs(15 * 60 * 1000); // 15 minutes

Getting Started

Install one of two ways:

  • Download the repository zip from GitHub, extract it, and drop the folder into your PlatformIO lib/ directory, Arduino IDE libraries/ directory, or add it as an ESP-IDF component.
  • Add the public GitHub URL to lib_deps in platformio.ini so PlatformIO fetches it for you:
    lib_deps = https://github.com/ESPToolKit/esp-date.git
    

Then include the umbrella header:

#include<Arduino.h>
#include<ESPDate.h>// Create instances globally; configure them in setup once Arduino/WiFi are ready
ESPDate date;
ESPDate solar;
voidsetup() {
Serial.begin(115200);
// Configure TZ + NTP after WiFi is connected if you want ESPDate to call configTzTime
ESPDateConfig dateCfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
dateCfg.ntpServer2 = "time.google.com";
dateCfg.ntpServer3 = "time.cloudflare.com";
dateCfg.usePSRAMBuffers = true; // optional: best effort, falls back automatically on non-PSRAM boards
date.init(dateCfg);
// Single-server config remains valid as before.
ESPDateConfig solarCfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000};
solarCfg.usePSRAMBuffers = true;
solar.init(solarCfg);
date.setNtpSyncCallback([](const DateTime& syncedAtUtc) {
Serial.printf("NTP synced at %lld\n", static_cast<longlong>(syncedAtUtc.epochSeconds));
});
date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update: 10 minutes
date.syncNTP(); // optional: force an immediate re-syncif (date.hasLastNtpSync()) {
Serial.printf("Last NTP sync: %lld\n", static_cast<longlong>(date.lastNtpSync().epochSeconds));
char lastSyncBuf[32];
if (date.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))) {
Serial.printf("Last NTP sync local: %s\n", lastSyncBuf);
}
}
// Make sure system time is set up (SNTP / manual) before calling date.now()
DateTime now = date.now(); // current time from system clock
DateTime lastYear = date.subYears(1); // 1 year before nowint64_t diffSeconds = date.differenceInSeconds(now, lastYear);
int64_t diffDays = date.differenceInDays(now, lastYear);
bool isBefore = date.isBefore(lastYear, now); // truechar buf[32];
if (date.formatUtc(now, ESPDateFormat::Iso8601, buf, sizeof(buf))) {
Serial.print("Now (UTC): ");
Serial.println(buf);
}
Serial.print("Seconds between now and last year: ");
Serial.println(diffSeconds);
Serial.print("Days between now and last year: ");
Serial.println(diffDays);
LocalDateTime local = date.nowLocal(); // quick DST/local sanity checkif (local.ok) {
Serial.printf("Local now: %04d-%02d-%02d %02d:%02d:%02d (UTC offset %+d min)\n",
local.year, local.month, local.day,
local.hour, local.minute, local.second,
local.offsetMinutes
);
}
char localBuf[32];
if (date.nowLocalString(localBuf, sizeof(localBuf))) {
Serial.printf("Local now (string): %s\n", localBuf);
}
std::string utcString = date.nowUtcString();
Serial.printf("UTC now (string): %s\n", utcString.c_str());
}
voidloop() {
// Example teardown path (mode switch / OTA / feature shutdown).staticbool released = false;
if (!released && millis() > 60000UL) {
if (date.isInitialized()) {
date.deinit();
}
if (solar.isInitialized()) {
solar.deinit();
}
released = true;
}
}

Working With Local Time (UI) vs UTC (storage/logic)

  • Show users local values: format with formatLocal or break down with toLocal/nowLocal.
  • Store and compare UTC: keep DateTime as UTC epoch seconds so comparisons are consistent.
  • Converting user choices back to UTC:
// User picked "2025-03-05 21:30" in local time (UI)
DateTime when = date.fromLocal(2025, 3, 5, 21, 30, 0);
// or parse: date.parseDateTimeLocal("2025-03-05 21:30:00").value;// Store `when` (UTC) and compare to date.now()/sunset() etc.if (date.isAfter(date.now(), when)) {
Serial.println("Already passed");
}
// When showing it again, render local:char buf[32];
date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Scheduled for local time: %s\n", buf);

Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed DateTime. Use formatLocal/toLocal to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.

Date & Time Model

DateTime is a small value type representing a moment in time, backed by seconds since the Unix epoch:

structDateTime {
int64_t epochSeconds; // seconds since 1970-01-01T00:00:00ZintyearUtc() const;
intmonthUtc() const; // 1..12intdayUtc() const; // 1..31inthourUtc() const; // 0..23intminuteUtc() const; // 0..59intsecondUtc() const; // 0..59boolutcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const;
};

It is cheap to copy (just an int64_t), safe to compare and subtract, and convertible to/from struct tm internally by ESPDate. You never manipulate struct tm directly—always go through ESPDate.

DateTime lastYear = date.subYears(1);
char buf[32];
if (lastYear.localString(buf, sizeof(buf))) {
Serial.printf("Last year local: %s\n", buf);
}

API Overview

The main module-type class you will use:

classESPDate {
public:using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc);
using NtpSyncCallable = std::function<void(const DateTime& syncedAtUtc)>;
~ESPDate();
voidinit(const ESPDateConfig &config);
voiddeinit();
boolisInitialized() const;
voidsetNtpSyncCallback(NtpSyncCallback callback);
template <typename Callable>
voidsetNtpSyncCallback(Callable&& callback); // capturing lambda/std::bind/functorboolsetNtpSyncIntervalMs(uint32_t intervalMs);
boolhasLastNtpSync() const;
DateTime lastNtpSync() const;
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
boolremoveNtpSyncListener(NtpSyncListenerId id);
boolsyncNTP();
booldateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
booldateTimeToStringLocal(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollocalDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const;
boolnowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boolnowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
boollastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string localDateTimeToString(const LocalDateTime &dt) const;
std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const;
std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const;
// Time sources
DateTime now() const;
DateTime fromUnixSeconds(int64_t seconds) const;
DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const;
int64_ttoUnixSeconds(const DateTime &dt) const;
// Arithmetic (UTC-backed)
DateTime addSeconds(const DateTime &dt, int64_t seconds) const;
DateTime addMinutes(const DateTime &dt, int64_t minutes) const;
DateTime addHours(const DateTime &dt, int64_t hours) const;
DateTime addDays(const DateTime &dt, int32_t days) const;
DateTime addMonths(const DateTime &dt, int32_t months) const;
DateTime addYears(const DateTime &dt, int32_t years) const;
DateTime subSeconds(const DateTime &dt, int64_t seconds) const;
DateTime subMinutes(const DateTime &dt, int64_t minutes) const;
DateTime subHours(const DateTime &dt, int64_t hours) const;
DateTime subDays(const DateTime &dt, int32_t days) const;
DateTime subMonths(const DateTime &dt, int32_t months) const;
DateTime subYears(const DateTime &dt, int32_t years) const;
// Convenience: relative to now()
DateTime addSeconds(int64_t seconds) const;
DateTime addMinutes(int64_t minutes) const;
DateTime addHours(int64_t hours) const;
DateTime addDays(int32_t days) const;
DateTime addMonths(int32_t months) const;
DateTime addYears(int32_t years) const;
DateTime subSeconds(int64_t seconds) const;
DateTime subMinutes(int64_t minutes) const;
DateTime subHours(int64_t hours) const;
DateTime subDays(int32_t days) const;
DateTime subMonths(int32_t months) const;
DateTime subYears(int32_t years) const;
// Differences & comparisonsint64_tdifferenceInSeconds(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInMinutes(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInHours(const DateTime &a, const DateTime &b) const;
int64_tdifferenceInDays(const DateTime &a, const DateTime &b) const;
boolisBefore(const DateTime &a, const DateTime &b) const;
boolisAfter(const DateTime &a, const DateTime &b) const;
boolisEqual(const DateTime &a, const DateTime &b) const;
boolisSameDay(const DateTime &a, const DateTime &b) const; // UTC calendar day// Calendar helpers (UTC and local variants)
DateTime startOfDayUtc(const DateTime &dt) const;
DateTime endOfDayUtc(const DateTime &dt) const;
DateTime startOfMonthUtc(const DateTime &dt) const;
DateTime endOfMonthUtc(const DateTime &dt) const;
DateTime startOfDayLocal(const DateTime &dt) const;
DateTime endOfDayLocal(const DateTime &dt) const;
DateTime startOfMonthLocal(const DateTime &dt) const;
DateTime endOfMonthLocal(const DateTime &dt) const;
DateTime startOfYearUtc(const DateTime &dt) const;
DateTime startOfYearLocal(const DateTime &dt) const;
DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const;
DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const;
DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const;
DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const;
intgetYearUtc(const DateTime &dt) const;
intgetMonthUtc(const DateTime &dt) const; // 1..12intgetDayUtc(const DateTime &dt) const; // 1..31intgetWeekdayUtc(const DateTime &dt) const; // 0=Sun..6=SatintgetYearLocal(const DateTime &dt) const;
intgetMonthLocal(const DateTime &dt) const;
intgetDayLocal(const DateTime &dt) const;
intgetWeekdayLocal(const DateTime &dt) const;
boolisLeapYear(int year) const;
intdaysInMonth(int year, int month) const; // month: 1..12// FormattingboolformatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const;
boolformatWithPatternUtc(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
boolformatWithPatternLocal(const DateTime &dt, constchar *pattern, char *outBuffer, size_t outSize) const;
structParseResult { bool ok; DateTime value; };
ParseResult parseIso8601Utc(constchar *str) const; // "YYYY-MM-DDTHH:MM:SSZ"
ParseResult parseDateTimeLocal(constchar *str) const; // "YYYY-MM-DD HH:MM:SS"
};

Examples

Full sketches:

  • examples/basic_date/basic_date.ino for broad API coverage.
  • examples/string_helpers/string_helpers.ino for buffer + std::string formatting APIs (including direct DateTime/LocalDateTime methods).
  • examples/ntp_sync_tracking/ntp_sync_tracking.ino for syncNTP, callback handling, and lastNtpSyncStringLocal/Utc.

Difference between timestamps:

DateTime now = date.now();
DateTime yesterday = date.subDays(1);
int64_t sec = date.differenceInSeconds(now, yesterday);
int64_t min = date.differenceInMinutes(now, yesterday);
int64_t days = date.differenceInDays(now, yesterday);
Serial.printf("Δ: %lld s, %lld min, %lld days\n",
static_cast<longlong>(sec),
static_cast<longlong>(min),
static_cast<longlong>(days)
);

Start/end of day (local):

DateTime now = date.now();
DateTime start = date.startOfDayLocal(now);
DateTime end = date.endOfDayLocal(now);
char buf[32];
date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day starts at: ");
Serial.println(buf);
date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.print("Day ends at: ");
Serial.println(buf);

Calculating the next month’s billing date:

DateTime now = date.now();
DateTime thisBilling = date.setTimeOfDayLocal(
date.startOfMonthLocal(now),
3, 0, 0// 03:00 local on the 1st
); DateTime nextBilling = date.addMonths(thisBilling, 1);

Formatting standalone values without ESPDate round-trips:

DateTime lastYear = date.subYears(1);
char localBuf[32];
if (lastYear.localString(localBuf, sizeof(localBuf))) {
Serial.printf("Last year local: %s\n", localBuf);
}
LocalDateTime local = date.toLocal(lastYear);
std::string localString = local.localString();
Serial.printf("LocalDateTime string: %s\n", localString.c_str());

Sunrise / Sunset

Bind your coordinates and TZ once via init, then fetch today’s sun cycle (auto-DST):

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg);
SunCycleResult rise = solar.sunrise(); // today, using stored config
SunCycleResult setToday = solar.sunset(); // today, using stored config
SunCycleResult setOnDate = solar.sunset(date.fromUtc(2024, 6, 1)); // specific dayif (rise.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
}

Or call with explicit parameters:

// Numeric offset + DST flag
SunCycleResult nycRise = date.sunrise(40.7128f, -74.0060f, -5.0f, true, date.fromUtc(2024, 7, 1));
// POSIX TZ string (auto-DST for that zone)
SunCycleResult nycRiseTz = date.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2");
// Daylight check (inclusive between sunrise and sunset; offsets adjust both ends)bool isNowDay = solar.isDay(); // uses stored configbool isGivenDay = solar.isDay(date.fromUtc(2024, 6, 1));
bool isWithOffsets = solar.isDay(-900, -1800); // 15 min before sunrise, 30 min before sunsetbool dstNow = solar.isDstActive(); // stored TZ or current system TZbool dstForDate = date.isDstActive(
date.fromUtc(2024, 10, 1, 12, 0, 0),
"EST5EDT,M3.2.0/2,M11.1.0/2"
);
// Moon phase (angle in degrees, illumination 0..1)
MoonPhaseResult phase = date.moonPhase();
if (phase.ok) {
Serial.printf("Moon angle: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination);
}
// Month names (UTC calendar)constchar* month = date.monthName(date.now()); // e.g., "March"

When the sun never rises/sets for that day (e.g., polar regions), ok will be false.

Scheduler-friendly helpers

  • Compute the next local run at HH:MM:SS, rolling to tomorrow if needed:
DateTime now = date.now();
DateTime nextRun = date.nextDailyAtLocal(3, 0, 0, now); // next 03:00 local
  • Compute the next Monday 09:30 local (weekday: 1 = Monday):
DateTime nextMonday = date.nextWeekdayAtLocal(1, 9, 30, 0, now);
  • Truncate to the start of a period:
DateTime startDay = date.startOfDayLocal(now);
DateTime startYear = date.startOfYearLocal(now);

Sun cycle example

See examples/sun_cycle/sun_cycle.ino for a full sketch. Key bits:

ESPDate solar;
ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"};
cfg.ntpServer2 = "time.google.com";
cfg.ntpServer3 = "time.cloudflare.com";
solar.init(cfg); // call in setup after WiFi
DateTime today = solar.now();
SunCycleResult rise = solar.sunrise(today);
SunCycleResult set = solar.sunset(today);
if (rise.ok && set.ok) {
char buf[32];
solar.formatLocal(rise.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunrise: %s\n", buf);
solar.formatLocal(set.value, ESPDateFormat::DateTime, buf, sizeof(buf));
Serial.printf("Sunset : %s\n", buf);
}

Gotchas

  • ESPDate configures SNTP only when you call init with timeZone and at least one configured NTP server (ntpServer, ntpServer2, or ntpServer3) in ESPDateConfig (it calls configTzTime). Empty server strings are ignored and compacted. Call it after WiFi is up, or ensure the device clock is set before calling now(). Sunrise/sunset use either the stored TZ string (if provided) or the current process TZ; make sure it matches the coordinates you pass.
  • All arithmetic and comparisons are UTC-first. Local helpers rely on the current process TZ (setenv("TZ", ...), tzset()); make sure that matches your deployment.
  • Month/year arithmetic clamps to the last valid day of the target month (e.g., Jan 31 + 1 month → Feb 28/29; Feb 29 - 1 year → Feb 28).
  • differenceInDays is purely seconds / 86400 truncated toward zero, not a calendar-boundary delta.
  • Leap seconds are treated like 60th seconds in parsing; they are not modeled beyond that.
  • isSameDay compares the UTC calendar day. Use startOfDayLocal / endOfDayLocal if you need local-day comparisons.
  • Buffer-first formatting APIs avoid extra dynamic formatting allocations and return false when buffers are too small or conversion fails.
  • usePSRAMBuffers affects ESPDate-owned text buffers only; std::string convenience return values and callback captures may still allocate through toolchain/STL defaults.
  • ESP32 toolchains typically ship a 64-bit time_t; on 32-bit time_t toolchains dates beyond 2038 may overflow (a compile-time warning is emitted).
  • differenceInDays(a, b) is defined as floor((a - b) / 86400) on UTC seconds, not calendar boundaries.
  • SunCycleResult.ok is false when there is no sunrise/sunset for the given day/coordinates (e.g., polar night/day).

Restrictions

  • ESP32 + FreeRTOS (Arduino-ESP32 or ESP-IDF) with C++17 enabled.
  • Requires a working system clock (time()) and relies on POSIX-style TZ handling for local-time helpers.

Tests

  • CI builds examples via PlatformIO and Arduino CLI on common ESP32 boards to ensure the API compiles cleanly under ArduinoJson-installed environments.
  • When using Arduino CLI locally, mirror CI by priming the ESP32 board manager URL before installing the core:
    arduino-cli config init --overwrite
    arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    arduino-cli core install esp32:esp32@3.3.3 --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • You can also run pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17" locally.
  • Unity smoke tests live in test/test_esp_date; run them on hardware with pio test -e esp32dev (or your board environment) to exercise arithmetic, formatting, and parsing routines.

Formatting Baseline

This repository follows the firmware formatting baseline from esptoolkit-template:

  • .clang-format is the source of truth for C/C++/INO layout.
  • .editorconfig enforces tabs (tab_width = 4), LF endings, and final newline.
  • Format all tracked firmware sources with bash scripts/format_cpp.sh.

License

MIT — see LICENSE.md.

ESPToolKit

About

A tiny C++17 helper for ESP32 projects that makes working with dates and times feel more like using date-fns in JavaScript/TypeScript.

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages